raidz: fix space accounting after expansion - #18324
Conversation
efca6c5 to
2b9d11e
Compare
|
Maybe it would be worthwhile to add some explicit tests of this functionality, to verify that the amount of data required to fill a pool is approximately equal to the remaining freespace that is reported? For example, create a pool and then do a series of large writes interspersed with raidz expansions, while verifying that the size of the write, the amount of freespace remaining, and the amount of space consumed before/after are all within some tolerance of each other? It might also be worthwhile doing a version with where you do a ZFS rewrite each time inbetween expansions. |
2b9d11e to
e7b3074
Compare
|
@owlshrimp Good idea — added raidz_expand_009_pos (multi-expansion with writes between each step) and raidz_expand_010_pos (rewrites between expansions + fills pool to verify freespace accuracy). |
I suppose it would be more correct to say that the reported freespace before vs after should be approximately equal to the size of the write (with compression turned off, anyway). The various cases of "how much space should this have taken" with different stripe sizes makes my head hurt though, especially if you delete old data. Deleting old data is probably another area where tests verifying expected behavior would be useful. |
|
Mh, the two new tests cover both. Test 010 does delete+rewrite cycles between expansions, then fills the pool to verify reported freespace actually matches writable capacity. Let me know if you think that covers it or if you had something else in mind. |
Reading through the testing strategies, I think this probably covers everything I can think of offhand. Maybe there are weird edgecases with mixes of pre and post expansion data, but 09 seems like at least a very good sanity check. What would we expect to happen to the total figures if some quantity of pre-expansion data is deleted? I presume the freespace would go up by a bit more than the amount of data deleted, due to the difference in parity ratio? |
Right, pre-expansion blocks use more raw space per logical byte (worse parity ratio), so deleting them frees raw capacity that under the new geometry maps to more usable space. That's the convergence the commit message describes: as old blocks get deleted or rewritten, accounting gradually aligns with the new geometry. |
|
I suppose every time you do an expansion (with these changes) the pre-expansion blocks belonging to files and snapshots look like they consume slightly more usable space than expected for their logical size. Have you double checked things like the amount of space reported as consumed by old snapshots whose files have been deleted from the current view makes sense in some way? (eg. |
|
Yes, pre-expansion blocks genuinely cost more raw space per logical byte (worse parity ratio), so that's expected and correct. Good call on snapshots though, I don't have explicit test coverage for that. I'll add one just after dinner (UTC+1 here!) |
e7b3074 to
70330f7
Compare
|
Also added an export/import check to test 008 — vdev_deflate_ratio_current isn't persisted on disk, it's recomputed during vdev_open() on import. So if that recomputation ever regressed, all accounting would silently break after a reboot or pool reimport. Now verified it survives the cycle. |
|
I guess every time you delete a pre-expansion file that exists in a snapshot, the snapshot grows by the same slightly-more-than-logical amount of raw size as the file used to occupy? |
70330f7 to
c5f8bf9
Compare
|
Yes exactly, the snapshot's used increases by the dsize of those blocks, which uses the old deflation ratio (via birth txg). So it reflects the actual raw cost of holding pre-expansion data. And test 11 already verifies this. |
|
Assuming one rewrote all the data (and didn't keep any snapshots) are there any lingering differences between an expanded array and one that was constructed natively, given this patchset? |
|
No, from this patchset's perspective there are no lingering differences. Once all data is rewritten under the new geometry, the per-block deflation ratios all reflect the current layout (identical to native), and the SPA-level capacity correction produces the same reported size as a natively constructed array. The expansion feature itself does leave persistent metadata on disk (e.g., the raidz_expansion feature flag, expansion history), but that is unrelated to this patchset and does not affect space accounting or I/O behavior. |
68c0a4d to
0ecea7f
Compare
|
This also follows up on the "time dependent deflate ratio" concept originally described by @ahrens in #12225 (comment) |
73cbd85 to
28b6f95
Compare
5f6c338 to
24a2224
Compare
|
How do I apply this patch? Or is there a big chance this will find its way into an official openzfs release soon? |
This patch is not ready yet and is still work in progress. If you attempt to apply it you are putting your data at risk. After it's complete and passes review it will be merged into the main branch, and presumably will ship in a new release soon after that. |
24a2224 to
2b04764
Compare
|
Hey, just wanted to follow up. I rebased and repushed yesterday so it should be clean on top of current main. |
|
I am not a ZFS developer, but it makes sense not to squash commits during the review phase at least, since it's hard to understand what changed in the last commit. |
The contributing guidelines seems to disagree with you: Line 131 in 5dd9121 But I agree that it can complicate things when the change spans multiple independent concerns. Here, the fix and its test are a single logical unit (and there was actually nothing to squash), so a single commit is both the rule and the right call. That said, it is still possible to review the changes file-by-file if looking at everything at once seems overwhelming. |
|
@amotin @behlendorf @ahrens This seems like a really useful series and a final missing piece for raidz expansion. Is there someone who might have the time to review this? |
behlendorf
left a comment
There was a problem hiding this comment.
@Skountz would you mind rebasing this again and addressing the spa_feature_t enum reordering and related abi updates while you're at it. I'll try and make time for a full review this week or next.
1b5918e to
e624f6b
Compare
The post-rebase checkABI passed, and everything is in order, I'm running the tests on a VM while the CI is doing its job just in case, but I guess we're good for the review once those are passing! |
|
It seems some qemu checks are failing? |
|
Another rebase on master should sort out the CI failures. |
e624f6b to
84c0d3f
Compare
|
that issue with dedup looks......hairy. I hope it doesn't have bearing against this work, but I wouldn't bet against it. |
@owlshrimp no bearing here, they're complementary. That mixed-epoch case (a dedup entry born before expansion, extended with a post-expansion DVA) is exactly what this PR's per-block accounting handles: each DVA's size comes from its own stored ASIZE and the block's physical birth, both immutable, so a block is freed with the same deflated size it was born with. No leak. #18826 fixes a different path, where RAIDZ uses that single birth to pick the stripe width when reading the DVA. That's an I/O safety issue, not an accounting one. Meanwhile I'm working through behlendorf's review here: making the feature READONLY_COMPAT instead of MOS. Testing now. |
After RAIDZ expansion, the deflation ratio used for space accounting remains at the original (pre-expansion) geometry because vdev_set_deflate_ratio() intentionally hard-codes txg 0 to avoid inconsistently accounting for existing blocks. This causes zpool list to underreport usable capacity. For example, expanding a 4-disk RAIDZ2 to 5 disks improves the data-to-parity ratio from 2/4 to 3/5, a ~20% increase in usable capacity per unit of raw storage, but the reported capacity does not reflect this improvement. Fix this in three parts: 1. Add vdev_deflate_ratio_current to vdev_t, computed using UINT64_MAX (current geometry). Apply a dspace correction in spa_update_dspace() and spa_prop_get_config() using the difference between the two ratios on the FREE portion of each expanded vdev. Only the free portion is corrected because dd_used_bytes tracks old-geometry blocks at the legacy ratio via bp_get_dsize_sync(); correcting allocated space would inflate available space beyond what can physically be written. As old blocks are rewritten and vs_alloc shrinks, the correction automatically grows, converging to the full geometry correction after a complete rewrite. The correction is scoped to the normal metaslab class because spa_rdspace derives from spa_normal_class(); special and dedup classes contribute only allocated (not free) space to spa_dspace via metaslab_class_get_dalloc(), so they do not need a free-space correction. The original txg-0 ratio is preserved in vdev_deflated_space() for self-consistent metaslab accounting and persistent DN_USED_BYTES tracking. 2. Add vdev_get_deflate_ratio(vd, birth_txg) which returns the correct deflation ratio for a block based on its physical birth txg. For non-expanded vdevs the cached ratio is returned immediately; for expanded RAIDZ, the geometry at birth is looked up via vdev_psize_to_asize_txg() and the existing reflow_node_t expansion history AVL tree. Modify dva_get_dsize_sync() to accept a birth_txg parameter and bp_get_dsize_sync() to use BP_GET_PHYSICAL_BIRTH(), which reflects the actual on-disk allocation geometry even for dedup/clone blocks whose logical birth may differ. 3. Add the raidz_expansion_accounting feature flag, which records the txg at which per-block deflation ratio tracking was enabled. Blocks born before this txg use the legacy fixed ratio (matching how they were originally accounted); blocks born at or after use the per-birth-txg ratio. This prevents born/free accounting mismatches for blocks written between a pre-patch expansion and the first expansion with this code. The feature follows the standard OpenZFS lifecycle: the user enables it via zpool upgrade or zpool set, and it is activated on the next RAIDZ expansion. If enabled on an already-expanded pool, it activates immediately in feature_enable_sync(). The enabled_txg is cached in spa_raidz_expand_acct_txg for efficient runtime lookup. The three changes together maintain self-consistency: metaslab accounting uses the original ratio (alloc/free always balance), capacity reporting is corrected at the spa level, and per-block accounting uses stable birth-txg ratios. No negative overflows or positive leaks are possible. As old blocks are rewritten to the new geometry, the accounting converges to exact values. Signed-off-by: Skountz <dev@frenchbytes.fr>
84c0d3f to
3f6f238
Compare
Does that mean this work might close that corner case, or would more work likely be required to fix that particular issue? |
No, and it's not meant to. This PR is purely about space accounting; it guarantees a block is freed with the same deflated size it was born with, per DVA. It never touches the read path, so it does nothing to prevent the wrong-geometry read that #18826 addresses. That mixed-epoch block pointer still shouldn't exist, and #18826 is what stops it from being created (and contains pools that already carry one). The two are independent and both needed; neither closes the other. |
Motivation and Context
After RAIDZ expansion, usable capacity is severely underreported because the deflation ratio (which converts raw
physical space to logical usable space) remains stuck at the original pre-expansion geometry. Both
zfs list -o availableandzpool list -o usable,availableshow values based on the old, less efficient parity layout.Fixes #17784
Fixes #18199
Description
vdev_set_deflate_ratio()hard-codes txg 0 to compute the ratio. After expansion, new writes use a more efficientgeometry but the accounting never reflects this.
Fix in three parts:
Add
vdev_deflate_ratio_current(computed with UINT64_MAX) alongside the existingvdev_deflate_ratio(txg0). Apply a capacity correction inspa_update_dspace()(forzfs listavailable space, quota enforcement, and space reservations) and inspa_prop_get_config()(forZPOOL_PROP_USABLE/ZPOOL_PROP_AVAILABLE), using the difference between the two ratios on the free portion of each expanded vdev. Only the free portion is corrected becausedd_used_bytestracks old-geometry blocks at the legacy ratio; correcting allocated space would inflate available space beyond what can physically be written, which could cause pool suspension on a full pool. As old blocks are rewritten andvs_allocshrinks, the correction automatically grows, converging to the full geometry correction after a complete rewrite.The original txg-0 ratio is preserved in
vdev_deflated_space()for self-consistent metaslab accounting and persistent DN_USED_BYTES tracking.Note:
zpool listdefault columns (SIZE,ALLOC,FREE) show raw physical space and are not deflation-adjusted; this is by design and unchanged.Add
vdev_get_deflate_ratio(vd, birth_txg)for per-block deflation usingBP_GET_PHYSICAL_BIRTH(). Pass birth txg throughdva_get_dsize_sync()andbp_get_dsize_sync(). Reuses existingreflow_node_tAVL tree infrastructure.Add the
raidz_expansion_accountingfeature flag (depends onenabled_txg). Records the txg at which per-block deflation ratio tracking was enabled. Blocks born before that txg use the legacy fixed ratio (matching how they were originally accounted); blocks born at or after use the per-birth-txg ratio. Follows the standard OpenZFS feature lifecycle: the user enables it viazpool upgradeorzpool set, and it is activated on the next RAIDZ expansion viaspa_feature_incr()invdev_raidz_attach_sync(). If enabled on an already-expanded pool, it activates immediately infeature_enable_sync()(following theSPA_FEATURE_HEAD_ERRLOGprecedent). Prevents born/free accounting mismatches for blocks written between a pre-patch expansion and the first expansion with this code.Self-consistency is preserved: metaslab accounting uses the original ratio (alloc/free always balance), capacity
reporting is corrected at the spa level, and per-block accounting uses stable birth-txg ratios. No negative overflows
or positive leaks.
How Has This Been Tested?
make checkstyleclean.raidz_expand_008_pos: Creates RAIDZ2-3 (1 data + 2 parity), writes 300MB, expands to 4 disks, verifies usable capacity increase (~100%) exceeds the raw 33% disk addition (confirming the deflation ratio correction is applied toZPOOL_PROP_USABLE), verifieszfsdataset available also reflects the correction (exercises thespa_update_dspace()code path), checks alloc+free≈size consistency, verifies all accounting survives an export/import cycle, and verifies the USABLE property is not over-corrected (gap between USABLE and the geometric maximum must be proportional to fill level, catching any correction applied to allocated space instead of only free space).raidz_expand_009_pos: Multiple sequential expansions (RAIDZ1 3→4→5→6 disks) with writes interspersed between each. Verifies that consumed space matches write size, free space decrease tracks consumed increase, and alloc+free≈size holds at every step.raidz_expand_010_pos: Rewrite-between-expansions test (RAIDZ1 3→4→5). Deletes and rewrites all data after each expansion to force blocks from old geometry to new. Then fills the pool to capacity and verifies that total data written matcheszfs available(deflated) within 30% tolerance.raidz_expand_011_pos: Snapshot accounting across expansions. Takes snapshots before/between two expansions, deletes files from the active dataset, and verifies that snapshot referenced/used sizes remain stable and consistent. Also verifies pool-level accounting holds after snapshot destruction.raidz_expand_012_pos: Feature flag lifecycle and born/free leak detection. Verifiesraidz_expansion_accountingis enabled on new pools, activates on expansion, and survives export/import. Tests that deleting data written under both old and new geometries returns both pool-level allocated and per-dataset referenced to near-zero (no born/free accounting leak viabp_get_dsize_sync). Also tests second expansion, stress write/delete cycles, and the post-expansion enablement path (feature disabled at pool creation, expansion, then enablement triggers immediate activation).Types of changes
Note:
spa_feature_tenum changed (newSPA_FEATURE_RAIDZ_EXPANSION_ACCOUNTING), which updatesspa_feature_tablesize and thelibzfs.abi/libzfs_core.abifiles accordingly.dva_get_dsize_sync()signature changed andvdev_get_deflate_ratio()was added, but these are kernel-internal symbols (EXPORT_SYMBOL) only.Checklist: